#pragma once
#include <cctype>
#include <cstddef>
#include <cstdint>
#include <ios>
#include <memory>
#include <sstream>
#include <stdexcept>
#include <string>
#include <type_traits>
#include <unordered_map>
#include <utility>
#include <vector>

namespace xncc {
namespace options {
    enum class element_type : uint8_t
    {
        NONE,
        BOOL,
        INT8,
        UINT8,
        INT16,
        UINT16,
        INT32,
        UINT32,
        INT64,
        UINT64,
        FLOAT,
        DOUBLE,
        LDOUBLE,
        STRING,
    };
    template <typename T>
    struct is_option_args_type
        : std::integral_constant<
              bool,
              std::is_arithmetic<T>::value ||
                  std::is_same<typename std::remove_cv<typename std::remove_reference<T>::type>::type, char*>::value ||
                  std::is_same<typename std::remove_cv<typename std::remove_reference<T>::type>::type,
                               std::string>::value ||
                  std::is_same<typename std::remove_cv<typename std::remove_reference<T>::type>::type,
                               const char*>::value>
    {
    };
    class value_type : public std::enable_shared_from_this<value_type>
    {
      public:
        using s_ptr                           = std::shared_ptr<value_type>;
        virtual bool         is_array() const = 0;
        virtual element_type type() const
        {
            return element_type::NONE;
        }

        virtual std::string default_value_str() const
        {
            return "";
        }

        bool has_default() const
        {
            return has_default_;
        }

        virtual void from_string(const std::vector<std::string>&)
        {
        }
        virtual ~value_type() = default;

      protected:
        bool has_default_ = false;
    };

    template <typename T>
    constexpr element_type to_element_type()
    {
        using real_type_t = typename std::remove_cv<typename std::remove_reference<T>::type>::type;

        return std::is_same<real_type_t, bool>::value               ? element_type::BOOL :
               std::is_same<real_type_t, int8_t>::value             ? element_type::INT8 :
               std::is_same<real_type_t, uint8_t>::value            ? element_type::UINT8 :
               std::is_same<real_type_t, int16_t>::value            ? element_type::INT16 :
               std::is_same<real_type_t, uint16_t>::value           ? element_type::UINT16 :
               std::is_same<real_type_t, int32_t>::value            ? element_type::INT32 :
               std::is_same<real_type_t, uint32_t>::value           ? element_type::UINT32 :
               std::is_same<real_type_t, int64_t>::value            ? element_type::INT64 :
               std::is_same<real_type_t, uint64_t>::value           ? element_type::UINT64 :
               std::is_same<real_type_t, long long>::value          ? element_type::INT64 :
               std::is_same<real_type_t, unsigned long long>::value ? element_type::UINT64 :
               std::is_same<real_type_t, float>::value              ? element_type::FLOAT :
               std::is_same<real_type_t, double>::value             ? element_type::DOUBLE :
               std::is_same<real_type_t, long double>::value        ? element_type::LDOUBLE :
               std::is_same<real_type_t, std::string>::value        ? element_type::STRING :
               std::is_same<real_type_t, char*>::value              ? element_type::STRING :
               std::is_same<real_type_t, const char*>::value        ? element_type::STRING :
                                                                      element_type::NONE;
    }

    template <typename T>
    inline T cast_string(const std::string& s)
    {
        throw std::logic_error("cant not parse unkown type");
    }

    template <>
    inline int8_t cast_string(const std::string& s)
    {
        return static_cast<int8_t>(std::stoi(s));
    }

    template <>
    inline uint8_t cast_string(const std::string& s)
    {
        return static_cast<uint8_t>(std::stoi(s));
    }
    template <>
    inline int16_t cast_string(const std::string& s)
    {
        return static_cast<int16_t>(std::stoi(s));
    }

    template <>
    inline uint16_t cast_string(const std::string& s)
    {
        return static_cast<uint16_t>(std::stoi(s));
    }

    template <>
    inline int32_t cast_string(const std::string& s)
    {
        return std::stoi(s);
    }

    template <>
    inline uint32_t cast_string(const std::string& s)
    {
        return static_cast<uint32_t>(std::stoi(s));
    }

    template <>
    inline int64_t cast_string(const std::string& s)
    {
        return std::stoll(s);
    }

    template <>
    inline uint64_t cast_string(const std::string& s)
    {
        return std::stoull(s);
    }

    template <>
    inline long long cast_string(const std::string& s)
    {
        return std::stoll(s);
    }

    template <>
    inline unsigned long long cast_string(const std::string& s)
    {
        return std::stoull(s);
    }

    template <>
    inline float cast_string(const std::string& s)
    {
        return std::stof(s);
    }

    template <>
    inline double cast_string(const std::string& s)
    {
        return std::stod(s);
    }

    template <>
    inline long double cast_string(const std::string& s)
    {
        return std::stold(s);
    }

    template <>
    inline bool cast_string(const std::string& s)
    {
        return true;
    }

    template <>
    inline std::string cast_string(const std::string& s)
    {
        return s;
    }

    template <typename T>
    class single_type : public value_type
    {
      public:
        using ss_ptr = std::shared_ptr<single_type<T>>;
        bool is_array() const override
        {
            return false;
        }
        value_type::s_ptr default_value(T value)
        {
            value_       = value;
            has_default_ = true;
            return shared_from_this();
        }

        T value() const
        {
            return value_;
        }

        std::string default_value_str() const override
        {
            std::ostringstream oss;
            oss << std::boolalpha << value_;
            return oss.str();
        }

        element_type type() const override
        {
            return to_element_type<T>();
        }

        void from_string(const std::vector<std::string>& args) override
        {
            if (args.empty() && (std::is_same<T, bool>::value || std::is_same<T, const bool>::value)) {
                value_ = true;
            }
            else {
                value_ = cast_string<T>(args[0]);
            }
        }

      private:
        T value_;
    };

    template <typename T>
    class array_type : public value_type
    {
      public:
        using sa_ptr = std::shared_ptr<array_type<T>>;
        bool is_array() const override
        {
            return true;
        }
        value_type::s_ptr default_value(const std::vector<T>& value)
        {
            value_       = value;
            has_default_ = true;
            return shared_from_this();
        }

        std::string default_value_str() const override
        {
            std::ostringstream oss;
            oss << "{";
            for (auto& ele : value_) {
                oss << ele << ",";
            }
            oss << "}";
            return oss.str();
        }

        element_type type() const override
        {
            return to_element_type<T>();
        }

        std::vector<T> value() const
        {
            return value_;
        }

        void from_string(const std::vector<std::string>& args) override
        {
            value_.clear();
            for (const auto& arg : args) {
                value_.push_back(cast_string<T>(arg));
            }
        }

      private:
        std::vector<T> value_;
    };

    template <typename C>
    struct is_vector : std::false_type
    {
        using element_type = C;
    };
    template <typename T, typename A>
    struct is_vector<std::vector<T, A>> : std::true_type
    {
        static_assert(is_option_args_type<T>::value, "element type of vector must be option args type");
        using element_type = T;
    };

    template <typename T>
    inline typename std::enable_if<is_option_args_type<T>::value, std::shared_ptr<single_type<T>>>::type value()
    {
        return std::make_shared<single_type<T>>();
    }

    template <typename T>
    inline typename std::enable_if<is_vector<T>::value,
                                   std::shared_ptr<array_type<typename is_vector<T>::element_type>>>::type
    value()
    {
        return std::make_shared<array_type<typename is_vector<T>::element_type>>();
    }

    template <typename T>
    inline typename std::enable_if<is_option_args_type<T>::value, typename single_type<T>::ss_ptr>::type
    value_cast(value_type::s_ptr value)
    {
        if (to_element_type<T>() != value->type()) {
            throw std::runtime_error("single cast type error");
        }
        return std::dynamic_pointer_cast<single_type<T>>(value);
    }

    template <typename T>
    inline typename std::enable_if<is_vector<T>::value,
                                   typename array_type<typename is_vector<T>::element_type>::sa_ptr>::type
    value_cast(value_type::s_ptr value)
    {
        if (to_element_type<typename is_vector<T>::element_type>() != value->type()) {
            throw std::runtime_error("array cast type error");
        }
        return std::dynamic_pointer_cast<array_type<typename is_vector<T>::element_type>>(value);
    }

    static std::string& trim(std::string& s)
    {
        if (s.empty()) {
            return s;
        }
        s.erase(0, s.find_first_not_of(' '));
        s.erase(s.find_last_not_of(' ') + 1);
        return s;
    }

    static std::vector<std::string> split(const std::string& str, const std::string& pattern, bool trim_empty)
    {
        std::vector<std::string> ret;
        if (pattern.empty()) {
            return ret;
        }
        size_t start = 0;
        size_t index = str.find_first_of(pattern, 0);
        while (index != std::string::npos) {
            if (start != index) {
                auto tmp_str = str.substr(start, index - start);
                if (trim_empty) {
                    ret.push_back(trim(tmp_str));
                }
                else {
                    ret.push_back(tmp_str);
                }
            }
            start = index + pattern.size();
            index = str.find_first_of(pattern, start);
        }
        if (!str.substr(start).empty()) {
            auto tmp_str = str.substr(start);
            if (trim_empty) {
                ret.push_back(trim(tmp_str));
            }
            else {
                ret.push_back(tmp_str);
            }
        }
        return ret;
    }

    static std::string set_width(const std::string& s, std::size_t width, bool left)
    {
        if (s.size() >= width) {
            return s + std::string("\n") + std::string(width, ' ');
        }
        if (left) {
            std::string result(s);
            for (std::size_t i = s.size(); i < width; ++i) {
                result.append(" ");
            }
            return result;
        }
        std::string result;
        for (std::size_t i = s.size(); i < width; ++i) {
            result.append(" ");
        }
        return result + s;
    }

    struct option_arg
    {
        std::string       short_name;
        std::string       long_name;
        std::string       raw_name;
        std::string       description;
        value_type::s_ptr value = nullptr;

        option_arg(const option_arg& other) = default;

        option_arg(std::string       short_name_arg,
                   std::string       long_name_arg,
                   std::string       raw_name_arg,
                   std::string       description_arg = "",
                   value_type::s_ptr value_arg       = nullptr)
            : short_name(std::move(short_name_arg)),
              long_name(std::move(long_name_arg)),
              raw_name(std::move(raw_name_arg)),
              description(std::move(description_arg)),
              value(std::move(value_arg))
        {
        }

        using s_ptr = std::shared_ptr<option_arg>;

        std::string key() const
        {
            return short_name + long_name;
        }

        std::string help_info() const
        {
            std::string result;
            if (!short_name.empty()) {
                result += short_name;
                result += " ";
            }
            if (!long_name.empty()) {
                if (!result.empty()) {  // has short name
                    result += "[ ";
                    result += long_name;
                    result += " ]";
                }
                else {
                    result += long_name;
                }
            }
            if (value->type() != element_type::BOOL && short_name != "-h" && long_name != "--help") {
                result += " arg";
            }

            if (value->has_default()) {
                result += " (=";
                result += value->default_value_str();
                result += ")";
            }

            return set_width(result, 50, true) + description;
        }
    };

    class command_line_parser
    {
      public:
        command_line_parser()
        {
            add_option("h,help", value<bool>(), "print help info");
        }
        ~command_line_parser()                                           = default;
        command_line_parser(const command_line_parser& other)            = delete;
        command_line_parser& operator=(const command_line_parser& other) = delete;
        command_line_parser(command_line_parser&& other)                 = default;

        void parse(int argc, const char* argv[])
        {
            parse_help();
            auto is_key = [](const std::string& option_name) -> bool {
                if (option_name.size() == 2) {
                    return option_name[0] == '-' && std::isalpha(option_name[1]);
                }
                if (option_name.size() == 3) {
                    if (option_name[0] == '-' && option_name[1] == '-') {
                        throw std::logic_error("option name invalid");
                    }
                    return false;
                }
                if (option_name.size() >= 4) {
                    return option_name[0] == '-' && option_name[1] == '-' && std::isalpha(option_name[2]);
                }
                return false;
            };
            std::unordered_map<std::string, std::vector<std::string>> key_params;

            int i = 1;
            while (i < argc) {
                if (is_key(argv[i])) {
                    break;
                }
                ++i;
            }
            std::string last_key;
            while (i < argc) {
                const std::string arg = argv[i];
                if (is_key(arg)) {
                    last_key             = arg;
                    key_params[last_key] = {};
                }
                else {
                    key_params[last_key].push_back(arg);
                }
                ++i;
            }

            parse_key_params(key_params);
            has_parsed_ = true;
        }

        command_line_parser&
        add_option(const std::string& name, value_type::s_ptr value_type, const std::string& description)
        {
            if (has_parsed_) {
                throw std::logic_error("add option after parse");
            }
            auto option_arg = parse_option_arg(name);
            auto iter       = option_arg_key_values_.find(option_arg->key());
            if (iter != option_arg_key_values_.end()) {
                throw std::logic_error("repeated option name");
            }
            option_arg->description                   = description;
            option_arg->value                         = std::move(value_type);
            option_arg_key_values_[option_arg->key()] = option_arg;
            if (!option_arg->short_name.empty()) {
                option_arg_short_values_[option_arg->short_name] = option_arg;
            }
            if (!option_arg->long_name.empty()) {
                option_arg_long_values_[option_arg->long_name] = option_arg;
            }
            return *this;
        }

        std::string help_string() const
        {
            return help_string_;
        }

        bool exist(const std::string& name)
        {
            const auto option_name       = get_option_name(name);
            auto       iter_parsed_short = option_arg_parsed_short_values_.find(option_name);
            if (iter_parsed_short != option_arg_parsed_short_values_.end()) {
                return true;
            }
            auto iter_parsed_long = option_arg_parsed_long_values_.find(option_name);
            if (iter_parsed_long != option_arg_parsed_long_values_.end()) {
                return true;
            }

            auto iter_short = option_arg_short_values_.find(option_name);
            if (iter_short != option_arg_short_values_.end() && iter_short->second->value->has_default()) {
                return true;
            }

            auto iter_long = option_arg_long_values_.find(option_name);
            return iter_long != option_arg_long_values_.end() && iter_long->second->value->has_default();
        }

        template <typename T>
        auto get_option_value(const std::string& name) -> T
        {
            const auto option_name = get_option_name(name);
            if (!exist(name)) {
                throw std::runtime_error("can not find option " + option_name);
            }
            return get_stored_value<T>(option_name);
        }

        template <typename T>
        auto get_option_value(const std::string& name, const T& default_value) -> T
        {
            const auto option_name = get_option_name(name);
            if (!exist(name)) {
                return default_value;
            }
            return get_stored_value<T>(option_name);
        }

      private:
        template <typename T>
        auto get_stored_value(const std::string& option_name) -> T
        {
            auto              iter_short = option_arg_short_values_.find(option_name);
            auto              iter_long  = option_arg_long_values_.find(option_name);
            option_arg::s_ptr option_arg = nullptr;
            if (iter_short != option_arg_short_values_.end()) {
                option_arg = iter_short->second;
            }
            if (iter_long != option_arg_long_values_.end()) {
                option_arg = iter_long->second;
            }
            if (option_arg == nullptr) {
                throw std::runtime_error("can not find option " + option_name);
            }
            return value_cast<T>(option_arg->value)->value();
        }
        static option_arg::s_ptr parse_option_arg(const std::string& name)
        {
            auto option_names = split(name, ",", true);
            if (option_names.size() > 2 || option_names.empty()) {
                throw std::logic_error("option name invalid");
            }
            if (option_names.size() == 1) {
                const auto option_name = get_option_name(option_names[0]);
                if (option_name.size() == 2) {
                    return std::make_shared<option_arg>(option_name, "", name);
                }
                else if (option_name.size() > 2) {
                    return std::make_shared<option_arg>("", option_name, name);
                }
                else {
                    throw std::logic_error("option name invalid");
                }
            }
            else {
                const auto& option_name_1 = option_names[0];
                const auto& option_name_2 = option_names[1];
                if (option_name_1.size() > 1 && option_name_2.size() > 1) {
                    throw std::logic_error("option name invalid");
                }

                if (option_name_1.size() == 1 && option_name_2.size() == 1) {
                    throw std::logic_error("option name invalid");
                }

                if (option_name_1.size() == 1) {
                    return std::make_shared<option_arg>(get_option_name(option_name_1), get_option_name(option_name_2),
                                                        name);
                }

                if (option_name_2.size() == 1) {
                    return std::make_shared<option_arg>(get_option_name(option_name_2), get_option_name(option_name_1),
                                                        name);
                }
            }
            throw std::logic_error("option name invalid");
            return std::make_shared<option_arg>("", "", name);
        }
        void parse_key_params(const std::unordered_map<std::string, std::vector<std::string>>& key_params)
        {
            for (const auto& key_param : key_params) {
                const auto&       option_name = key_param.first;
                auto              iter_short  = option_arg_short_values_.find(option_name);
                auto              iter_long   = option_arg_long_values_.find(option_name);
                option_arg::s_ptr option_arg  = nullptr;
                if (iter_short != option_arg_short_values_.end()) {
                    option_arg = iter_short->second;
                }
                if (iter_long != option_arg_long_values_.end()) {
                    option_arg = iter_long->second;
                }
                if (option_arg == nullptr) {
                    throw std::logic_error("unkown option " + option_name);
                }

                const auto& params = key_param.second;
                if (!option_arg->value->is_array()) {
                    if (option_arg->value->type() == element_type::BOOL) {
                        if (!params.empty()) {
                            throw std::logic_error("option " + option_name +
                                                   " params size error, bool switch must not has params");
                        }
                    }
                    else {
                        if (params.empty() || params.size() != 1) {
                            throw std::logic_error("option " + option_name + " params size error, shouble be single");
                        }
                    }
                }
                else {
                    if (params.empty()) {
                        throw std::logic_error("option " + option_name + " params size error");
                    }
                }
                if (!option_arg->short_name.empty()) {
                    option_arg_parsed_short_values_[option_arg->short_name] = option_arg;
                }
                if (!option_arg->long_name.empty()) {
                    option_arg_parsed_long_values_[option_arg->long_name] = option_arg;
                }
                option_arg->value->from_string(params);
            }
        }
        static std::string get_option_name(const std::string& option_name)
        {
            auto is_invalid = [](const std::string& option_name) -> bool {
                if (!std::isalpha(option_name[0])) {
                    return false;
                }
                for (std::size_t i = 1; i < option_name.size(); ++i) {
                    if (!std::isalnum(option_name[i]) && option_name[i] != '_') {
                        return false;
                    }
                }
                return true;
            };

            if (!is_invalid(option_name)) {
                throw std::logic_error("option name invalid");
            }

            if (option_name.size() == 1) {
                return "-" + option_name;
            }
            if (option_name.size() > 1) {
                return "--" + option_name;
            }
            throw std::logic_error("option name invalid");
        }
        void parse_help()
        {
            std::ostringstream oss;
            oss << "Options:" << '\n';
            for (auto& option_arg : option_arg_key_values_) {
                oss << "  " << option_arg.second->help_info() << '\n';
            }
            help_string_ = oss.str();
        }

        std::unordered_map<std::string, option_arg::s_ptr> option_arg_key_values_;
        std::unordered_map<std::string, option_arg::s_ptr> option_arg_short_values_;
        std::unordered_map<std::string, option_arg::s_ptr> option_arg_long_values_;
        std::unordered_map<std::string, option_arg::s_ptr> option_arg_parsed_short_values_;
        std::unordered_map<std::string, option_arg::s_ptr> option_arg_parsed_long_values_;
        std::string                                        help_string_;
        bool                                               has_parsed_ = false;
    };

}  // namespace options

}  // namespace xncc